﻿using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Data.Entity.Design.PluralizationServices;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using LINQPad.Extensibility.DataContext;
using Microsoft.CSharp;

namespace MongoLP
{
    /// <summary>
    /// Much thanks to http://normlinqpaddriver.codeplex.com/ from which
    /// a great deal of this code is derived.
    /// 
    /// There is method to the madness.  This driver uses the standard MongoDB C#
    /// driver from 10gen (+ FluentMongo, and for good measure MongoRepository).
    /// NoRM is no longer actively developed.
    /// </summary>
    public class DynamicDriver : DynamicDataContextDriver
    {
        private static readonly PluralizationService PluralizationService = PluralizationService.CreateService(new CultureInfo("en-US"));
        private string _assemblyToAdd;

        /// <summary>
        /// Gets the name of the driver
        /// </summary>
        public override string Name
        {
            get { return "MongoLP - Dynamic LinqPad Driver"; }
        }

        /// <summary>
        /// Gets the author of the driver.
        /// </summary>
        public override string Author
        {
            get { return "Paul Saunders"; }
        }

        /// <summary>
        /// Gets the namespaces to add.
        /// </summary>
        /// <returns>
        /// A list of namespaces as <see cref="System.String"/>
        /// </returns>
        public override IEnumerable<string> GetNamespacesToAdd()
        {
            return new[]
                       {
                           "MongoDB.Driver",
                           "MongoDB.Bson",
                           "DreamSongs.MongoRepository",
                           "FluentMongo.Linq"
                       };
        }

        /// <summary>
        /// Gets the assemblies to add.
        /// </summary>
        /// <returns>
        /// A list of assembly names as <see cref="System.String"/>
        /// </returns>
        public override IEnumerable<string> GetAssembliesToAdd()
        {
            return new[]
            {
                Path.Combine(GetDriverFolder(), "MongoDB.Driver.dll"),
                Path.Combine(GetDriverFolder(), "MongoDB.Bson.dll"),
                Path.Combine(GetDriverFolder(), "FluentMongo.dll"),
                Path.Combine(GetDriverFolder(), "DreamSongs.MongoRepository.dll"),
                _assemblyToAdd
            };
        }

        /// <summary>
        /// Gets the connection description.
        /// </summary>
        /// <param name="cxInfo">The connection info.</param>
        /// <returns>A description of the connection.</returns>
        public override string GetConnectionDescription(IConnectionInfo cxInfo)
        {
            // I don't like this side effect, but it seems that there aren't too many places
            // to put it if you want to have it available elsewhere. :-(
            var props = new Properties(cxInfo);
            _assemblyToAdd = props.EntityAssemblyPath;
            return "MongoDb - " + Path.GetFileName(cxInfo.CustomTypeInfo.CustomAssemblyPath);
        }

        /// <summary>
        /// Shows the connection dialog.
        /// </summary>
        /// <param name="cxInfo">The connection information.</param>
        /// <param name="isNewConnection">if set to <c>true</c> the connection is new.</param>
        /// <returns>
        /// <c>true</c> if the dialog was shown correctly, <c>false</c> otherwise.
        /// </returns>
        public override bool ShowConnectionDialog(IConnectionInfo cxInfo, bool isNewConnection)
        {
            // Prompt the user for a custom assembly and type name:
            return new ConnectionDialog(cxInfo).ShowDialog() == true;
        }

        /// <summary>
        /// Gets the context constructor arguments.
        /// </summary>
        /// <param name="cxInfo">The connection information.</param>
        /// <returns>
        /// </returns>
        public override object[] GetContextConstructorArguments(IConnectionInfo cxInfo)
        {
            var props = new Properties(cxInfo);

            var assembly = Assembly.LoadFrom(Path.Combine(GetDriverFolder(), "MongoDB.Driver.dll"));
            var type = assembly.GetType("MongoDB.Driver.MongoDatabase");
            var create = type.GetMethod("Create", new[] { typeof(string) });

            // dynamically create a Mongo database object
            object mongo = create.Invoke(null, new object[] { props.ConnectionString });

            return new[] { mongo };
        }

        /// <summary>
        /// Gets the context constructor parameters.
        /// </summary>
        /// <param name="cxInfo">The connection information.</param>
        /// <returns></returns>
        public override ParameterDescriptor[] GetContextConstructorParameters(IConnectionInfo cxInfo)
        {
            var assembly = Assembly.LoadFrom(Path.Combine(GetDriverFolder(), "MongoDB.Driver.dll"));
            return new[] { new ParameterDescriptor("mongo", assembly.GetType("MongoDB.Driver.MongoDatabase").FullName) };
        }

        /// <summary>
        /// Gets the schema and build assembly.
        /// </summary>
        /// <param name="cxInfo">The connection information.</param>
        /// <param name="assemblyToBuild">The assembly to build.</param>
        /// <param name="nameSpace">The name space.</param>
        /// <param name="typeName">Name of the type.</param>
        /// <returns></returns>
        public override List<ExplorerItem> GetSchemaAndBuildAssembly(IConnectionInfo cxInfo, AssemblyName assemblyToBuild, ref string nameSpace, ref string typeName)
        {
            var props = new Properties(cxInfo);

            // load assembly
            var referencedAssemblies = new List<Assembly>();
            var assembly = Assembly.LoadFrom(props.EntityAssemblyPath);
            foreach (var a in assembly.GetReferencedAssemblies())
            {
                var localPath = Path.Combine(GetDriverFolder(), a.Name + ".dll");
                if (File.Exists(localPath))
                {
                    referencedAssemblies.Add(Assembly.LoadFrom(localPath));
                    continue;
                }

                var dllPath = Path.Combine(assembly.Location, a.Name + ".dll");
                if (File.Exists(localPath))
                {
                    referencedAssemblies.Add(Assembly.LoadFrom(dllPath));
                    continue;
                }

                if (Regex.Match(a.FullName, "linqpad", RegexOptions.IgnoreCase).Success)
                {
                    continue;
                }

                referencedAssemblies.Add(Assembly.Load(a.FullName));
            }

            // collect types
            string[] assemblyNamespaces = props.EntityInterface.Split(';');
            var entityTypes = new List<Type>();

            foreach (var r in referencedAssemblies)
            {
                foreach (var t in (r.GetTypes().Where(t => assemblyNamespaces.Contains(t.FullName))))
                {
                    foreach (var a in assemblyNamespaces)
                    {
                        var filteredTypes = assembly
                            .GetTypes()
                            .Where(x => x.IsPublic &&
                                          t.IsAssignableFrom(x));

                        entityTypes.AddRange(filteredTypes);
                    }

                    break;
                }
            }

            // Generate source
            string src = GenerateCode(entityTypes, nameSpace, typeName);

            // Build assembly
            BuildAssembly(src, props.EntityAssemblyPath, assemblyToBuild);

            // Generate schema
            return GenerateSchema(entityTypes);
        }

        /// <summary>
        /// Generates the schema.
        /// </summary>
        /// <param name="entityTypes">The entity types.</param>
        /// <returns></returns>
        private static List<ExplorerItem> GenerateSchema(List<Type> entityTypes)
        {
            var result = new List<ExplorerItem> { 
                GenerateObjectsCategory(entityTypes), 
                GenerateCollectionsCategory(entityTypes)
            };

            return result;
        }

        /// <summary>
        /// Generates the objects category.
        /// </summary>
        /// <param name="entityTypes">The entity types.</param>
        /// <returns></returns>
        private static ExplorerItem GenerateObjectsCategory(IEnumerable<Type> entityTypes)
        {
            var result = new ExplorerItem("Objects", ExplorerItemKind.QueryableObject, ExplorerIcon.Schema)
                             {Children = new List<ExplorerItem>()};

            foreach (Type t in entityTypes)
            {
                var typeItem = new ExplorerItem(t.Name, ExplorerItemKind.Property, ExplorerIcon.Table) 
                    { ToolTipText = string.Format("{0} Object", t.Name) };
                
                // add child nodes for properties
                PropertyInfo[] publicProperties = t.GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance);
                typeItem.Children = new List<ExplorerItem>();

                foreach (var info in publicProperties)
                {
                    var child = new ExplorerItem(info.Name, ExplorerItemKind.Property, ExplorerIcon.Column);
                    
                    typeItem.Children.Add(child);
                }

                result.Children.Add(typeItem);
            }

            return result;
        }

        /// <summary>
        /// Generates the collections category.
        /// </summary>
        /// <param name="entityTypes">The entity types.</param>
        /// <returns>
        /// An ExplorerItem for 
        /// </returns>
        private static ExplorerItem GenerateCollectionsCategory(IEnumerable<Type> entityTypes)
        {
            var result = new ExplorerItem("Collections", ExplorerItemKind.CollectionLink, ExplorerIcon.Schema)
                             {Children = new List<ExplorerItem>()};

            foreach (Type t in entityTypes)
            {
                var typeItem = new ExplorerItem(Pluralize(t.Name), ExplorerItemKind.Property, ExplorerIcon.Schema) { ToolTipText = string.Format("{0} Collection", t.Name) };
                result.Children.Add(typeItem);
            }

            return result;
        }

        /// <summary>
        /// Generates the code for LinqPad to use.
        /// </summary>
        /// <param name="entityTypes">The entity types.</param>
        /// <param name="nameSpace">The namespace to use.</param>
        /// <param name="typeName">Name of the type.</param>
        /// <returns>
        /// The code that was generated from the inputs.
        /// </returns>
        private static string GenerateCode(IList<Type> entityTypes, string nameSpace, string typeName)
        {
            var writer = new StringWriter();

            // write namespace and opening brace 
            if (!string.IsNullOrEmpty(nameSpace))
            {
                writer.Write("namespace ");
                writer.WriteLine(nameSpace);
                writer.WriteLine("{");
            }

            // write using directives
            writer.WriteLine("using System.Linq;");
            writer.WriteLine("using MongoDB.Driver;");
            writer.WriteLine("using FluentMongo.Linq;");
            writer.WriteLine("");

            // write type declaration
            writer.WriteLine(string.Format("public class {0}\n{{", typeName));
            writer.WriteLine(string.Format("\tpublic {0}(MongoDatabase mongo)\n\t{{", typeName));
            writer.WriteLine("\t\tthis.mongo = mongo;");
            writer.WriteLine("\t}\n");
            writer.WriteLine("\tprivate readonly MongoDatabase mongo;\n");

            // write Queryable and collection properties
            for (int i = 0; i < entityTypes.Count(); i++)
            {
                var type = entityTypes[i];
                var name = Pluralize(type.Name);

                writer.WriteLine(string.Format(
                    "\tpublic MongoCollection<{0}> {1} {{ get {{ return this.mongo.GetCollection<{0}>(\"{2}\"); }} }}\n", type.FullName, name, type.Name));

                writer.WriteLine(string.Format(
                    "\tpublic IQueryable<{0}> {1} {{ get {{ return this.mongo.GetCollection<{0}>(\"{1}\").AsQueryable(); }} }}\n", type.FullName, type.Name));
            }

            // write type closing brace 
            writer.WriteLine("}");

            // write namespace closing brace 
            if (!string.IsNullOrEmpty(nameSpace))
                writer.WriteLine("}");

            return writer.ToString();
        }

        /// <summary>
        /// Builds the assembly.
        /// </summary>
        /// <param name="code">The code.</param>
        /// <param name="entityAssembly">The entity assembly.</param>
        /// <param name="name">The name.</param>
        private void BuildAssembly(string code, string entityAssembly, AssemblyName name)
        {
            // Use the CSharpCodeProvider to compile the generated code:
            CompilerResults results;

            var assemblyNames = new List<string>();
            assemblyNames.AddRange("System.dll System.Core.dll".Split());
            assemblyNames.Add(entityAssembly);
            assemblyNames.Add(Path.Combine(GetDriverFolder(), "MongoDB.Driver.dll"));
            assemblyNames.Add(Path.Combine(GetDriverFolder(), "MongoDB.Bson.dll"));
            assemblyNames.Add(Path.Combine(GetDriverFolder(), "FluentMongo.dll"));
            assemblyNames.Add(Path.Combine(GetDriverFolder(), "DreamSongs.MongoRepository.dll"));

            using (var codeProvider = new CSharpCodeProvider(new Dictionary<string, string> { { "CompilerVersion", "v4.0" } }))
            {
                var options = new CompilerParameters(assemblyNames.ToArray(), name.CodeBase, true);
                results = codeProvider.CompileAssemblyFromSource(options, code);
            }

            if (results.Errors.Count > 0)
                throw new Exception("Cannot compile typed context: " + results.Errors[0].ErrorText + " (line " + results.Errors[0].Line + ")");
        }

        /// <summary>
        /// Pluralizes the specified name.
        /// </summary>
        /// <param name="name">The string to pluralize.</param>
        /// <returns>
        /// The plural of the string passed in.
        /// </returns>
        private static string Pluralize(string name)
        {
            return PluralizationService.Pluralize(name);
        }
    }
}
